/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.atomojo.www.util.script;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import org.atomojo.app.client.Feed;
import org.atomojo.app.client.FeedClient;
import org.atomojo.app.client.FeedDestination;
import org.atomojo.app.client.Link;
import org.atomojo.app.client.Term;
import org.infoset.xml.Document;
import org.infoset.xml.XMLException;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.MediaType;
import org.restlet.data.Reference;
import org.restlet.routing.Template;

/**
 *
 * @author alex
 */
public class ScriptManager {

   public static String ATTR = "org.atomojo.www.util.script.manager";
   
   public class ScriptContext {
      URI location;
      Transformer xform;
      MediaType type;
      ScriptContext(URI location,Transformer xform,MediaType type) {
         this.location = location;
         this.xform = xform;
         this.type = type;
      }
      
      public MediaType getMediaType() {
         return type;
      }
      public Transformer getTransformer() {
         return xform;
      }
      
      public void release() {
         cache.release(location, xform);
      }
   }
   
   public class Entry {
      Date edited;
      String id;
      Set<Term> criteria;
      String match;
      MediaType mediaType;
      URI location;
      int priority;
      Entry(String id,Date edited,Set<Term> criteria,String match,int priority,MediaType mediaType,URI location) {
         this.id = id;
         this.edited = edited;
         this.criteria = criteria;
         this.match = match;
         this.mediaType = mediaType;
         this.location = location;
         this.priority = priority;
      }
      
      public String getId() {
         return id;
      }
      public Date getEdited() {
         return edited;
      }
   }
   
   class TermSet {
      Set<Term> terms;
      long updated;
      TermSet() {
         terms = new TreeSet<Term>();
         updated = System.currentTimeMillis();
      }
   }
   class Result {
      Entry script;
      long updated;
      Result(Entry script) {
         this.script = script;
         updated = System.currentTimeMillis();
      }
   }
   Logger log;
   Client client;
   long expiration;
   Map<String,TermSet> termCache;
   Map<String,Result> resultCache;
   Map<String,Entry> scripts;
   Link metadata;
   String username;
   String password;
   ScriptCache cache;
   
   public ScriptManager(Context context,Client client,Link metadata,String username,String password,long expiration) {
      this.log = context.getLogger();
      this.client = client;
      this.scripts = new TreeMap<String,Entry>();
      this.termCache = new TreeMap<String,TermSet>();
      this.resultCache = new TreeMap<String,Result>();
      this.metadata = metadata;
      this.username = username;
      this.password = password;
      this.cache = new ScriptCache();
      this.expiration = expiration;
   }
   
   public ScriptContext findScript(Request request,String path) 
      throws IOException,TransformerException
   {
      Result result = resultCache.get(path);
      if (result!=null && (System.currentTimeMillis()-result.updated)<expiration) {
         return createContext(request,result.script);
      }
      TermSet termSet = termCache.get(path);
      if (termSet==null || (System.currentTimeMillis()-termSet.updated)>=expiration) {
         termSet = updateTermSet(path,termSet);
      }
      boolean isFineLog = log.isLoggable(Level.FINE);
      if (isFineLog) {
         for (Term t : termSet.terms) {
            log.fine("Feed term: "+t.getURI());
         }
      }
      
      List<Entry> matches = new ArrayList<Entry>();
      for (Entry entry : scripts.values()) {
         if (entry.match==null && entry.criteria.size()==0) {
            matches.add(entry);
            continue;
         }
         if (entry.match!=null) {
            if (isFineLog) {
               log.fine("Checking "+entry.match+" against path "+path);
            }
            Template template = new Template(entry.match);
            if (template.match(path)<0) {
               continue;
            }
            if (isFineLog) {
               log.fine("Matched layout entry "+entry.getId()+" using URI match "+entry.match);
            }
            if (entry.criteria.size()==0) {
               matches.add(entry);
            }
         }
         if (entry.criteria.size()>0) {
            if (isFineLog) {
               for (Term t : entry.criteria) {
                  log.fine("layout term: "+t.getURI());
               }
            }
            boolean ok = true; 
            for (Term term : entry.criteria) {
               boolean found = false;
               for (Term other : termSet.terms) {
                  if (other.getURI().equals(term.getURI())) {
                     found = true;
                     break;
                  }
               }
               if (!found) {
                  ok = false;
                  break;
               }
            }
            if (ok) {
               if (isFineLog) {
                  log.fine("Matched layout entry "+entry.getId()+", href="+entry.location);
               }
               matches.add(entry);
            }
         }
      }
      Entry matched = null;
      for (Entry entry : matches) {
         if (matched==null) {
            matched = entry;
         } else if (entry.priority>matched.priority) {
            matched = entry;
         }
      }
      if (matched!=null) {
         if (isFineLog) {
            log.fine("Using layout entry "+matched.getId()+", href="+matched.location);
         }
         if (result!=null) {
            result.updated = System.currentTimeMillis();
            result.script = matched;
         } else {
            result = new Result(matched);
            resultCache.put(path,result);
         }
      }
      return matched==null ? null : createContext(request,matched);
   }
   
   protected ScriptContext createContext(Request request,Entry script)
      throws IOException,TransformerException
   {
      Transformer xform = cache.get(script.location);
      // TODO: hack for browser detection.
      // TODO: browser detection should be part of the rule selection
      boolean isIE = request.getClientInfo().getAgent().indexOf("MSIE")>=0;
      return new ScriptContext(script.location,xform,isIE && script.mediaType==MediaType.APPLICATION_XHTML_XML ? MediaType.TEXT_HTML : script.mediaType);
   }
   
   protected TermSet updateTermSet(String path,TermSet termSet)
   {
      if (termSet==null) {
         termSet = new TermSet();
         termCache.put(path,termSet);
      }
      Reference ref = new Reference(metadata.getLink().toString()+path);
      log.info("Updating metadata for "+path+" from "+ref);
      FeedClient metadataClient = new FeedClient(client,ref);
      if (metadata.getUsername()!=null) {
         metadataClient.setIdentity(metadata.getUsername(),metadata.getPassword());
      }
      final Set<Term> updateSet = termSet.terms;
      try {
         Response response = metadataClient.get(new FeedDestination() {
            public void onFeed(Document feedDoc)
               throws XMLException
            {
               Feed feed = new Feed(feedDoc);
               feed.index();
               updateSet.clear();
               updateSet.addAll(feed.getTerms().values());
               if (log.isLoggable(Level.FINE)) {
                  for (Term t : feed.getTerms().values()) {
                     log.fine("term: "+t.getURI());
                  }
               }
            }
            public void onEntry(Document entryDoc) {
            }
         });
         if (!response.getStatus().isSuccess() && response.getStatus().getCode()!=404) {
            log.severe("Cannot get metadata from "+ref+", status="+response.getStatus().getCode());
            termSet.updated = 0;
         }
      } catch (Exception ex) {
         log.log(Level.SEVERE,"Cannot get feed metadata.",ex);
      }
      return termSet;
      
   }
   
   public Entry get(String id)
   {
      return scripts.get(id);
   }
   
   public void reload(String id,Date edited,Set<Term> criteria,String match,int priority,MediaType mediaType)
   {
      Entry entry = get(id);
      if (entry==null) {
         return;
      }
      log.fine("Reloaded script entry:\nid="+id+"\npriority="+priority+"\nmatch="+match+"\nmediaType="+mediaType+"\nlocation="+entry.location);
      for (Term t : criteria) {
         log.fine("criteria: "+t);
      }
      entry.criteria = criteria;
      entry.edited = edited;
      entry.match = match;
      entry.mediaType = mediaType;
      entry.priority = priority;
      cache.reload(entry.location);
   }
   
   public void add(String id,Date edited,Set<Term> criteria,String match,int priority,MediaType mediaType,URI location)
   {
      Entry entry = new Entry(id,edited,criteria,match,priority,mediaType,location);
      log.fine("Adding script entry:\nid="+id+"\npriority="+priority+"\nmatch="+match+"\nmediaType="+mediaType+"\nlocation="+entry.location);
      for (Term t : criteria) {
         log.fine("criteria: "+t);
      }
      cache.add(location, username, password, true);
      scripts.put(id,entry);
   }
   
   public Set<String> getKeys() {
      return scripts.keySet();
   }
   
   public void remove(String id) {
      Entry entry = scripts.remove(id);
      if (entry!=null) {
         List<String> toRemove = new ArrayList<String>();
         for (String path : resultCache.keySet()) {
            Result result = resultCache.get(path);
            if (result.script==entry) {
               toRemove.add(path);
            }
         }
         for (String path : toRemove) {
            resultCache.remove(path);
         }
         cache.remove(entry.location);
      }
   }
   
}
